#include <linux/errno.h>
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/netdevice.h>
#include <linux/of_platform.h>
#include <linux/phy.h>
#include <linux/gpio.h>
#include <linux/of_gpio.h>
#include <linux/cdev.h>
#include <uapi/linux/mii.h>
#include <linux/version.h>

#include "securec.h"
#include "lsw.h"
#include "drv_log.h"
#include "hns_mdio.h"

#define LSW_LOG_PREFIX LSW_CDEV_NAME
#define lsw_err(fmt...) drv_err(LSW_LOG_PREFIX, fmt)
#define lsw_warn(fmt...) drv_warn(LSW_LOG_PREFIX, fmt)
#define lsw_info(fmt...) drv_info(LSW_LOG_PREFIX, fmt)
#define lsw_debug(fmt...) drv_debug(LSW_LOG_PREFIX, fmt)

#define LSW_CDEV_MINORS 1
#define LSW_PHY_ID 0x3521

#ifndef CHECK_GOTO
#define CHECK_GOTO(a, v) do {  \
            if (!(a)) {        \
                goto v;      \
            }                  \
        } while (0)
#endif


static struct lsw_dev *lsw = NULL;

static int lsw_direct_read(struct mii_bus *bus, int devaddr, int regaddr)
{
    return mdiobus_read(bus, devaddr, regaddr);
}

static int lsw_direct_write(struct mii_bus *bus, int devaddr, int regaddr, u16 value)
{
    return mdiobus_write(bus, devaddr, regaddr, value);
}

static int lsw_wait_ready(struct mii_bus *bus)
{
    smi_cmd_reg smi_reg;
    int val = 0;
    int i;

    memset_s(&smi_reg, sizeof(smi_cmd_reg), 0, sizeof(smi_cmd_reg));

    for (i = 0; i < 20; i++) {
        val = lsw_direct_read(bus, SMI_GLOBAL2_DEVADDR, SMI_PHY_CMD_REG);
        if (val >= 0) {
            smi_reg = (smi_cmd_reg)(u16)val;
            if (smi_reg.bits.smibusy == SMI_IDLE)
                break;
        }
        msleep(10);
    }

    if (i >= 20) {
        lsw_info("rgmii marvell_check_phy_busy.\n");
        return -EBUSY;
    }
    return 0;
}

static int lsw_indirect_cmd(struct mii_bus *bus, int devaddr, int regaddr, int smicode)
{
    smi_cmd_reg smi_reg;
    int ret;

    ret = lsw_wait_ready(bus);
    if (ret) {
        return ret;
    }

    memset_s(&smi_reg, sizeof(smi_cmd_reg), 0, sizeof(smi_cmd_reg));
    smi_reg.bits.smibusy = SMI_BUSY;
    smi_reg.bits.smimode = SMI_CLAUSE_22; /* CLAUSE 22 */
    smi_reg.bits.smiop = smicode;         /* read */
    smi_reg.bits.devaddr = devaddr;
    smi_reg.bits.regaddr = regaddr;

    ret = lsw_direct_write(bus, SMI_GLOBAL2_DEVADDR, SMI_PHY_CMD_REG, smi_reg.value);
    if (ret < 0) {
        return ret;
    }

    ret = lsw_wait_ready(bus);
    return ret;
}

static int lsw_indirect_read(struct mii_bus *bus, int devaddr, int regaddr)
{
    int ret;

    ret = lsw_indirect_cmd(bus, devaddr, regaddr, SMI_READ);
    if (ret) {
        return ret;
    }

    ret = lsw_direct_read(bus, SMI_GLOBAL2_DEVADDR, SMI_PHY_DATA_REG);
    if (ret < 0)
        lsw_err("read: devaddr=0x%x regaddr=0x%x ret=%d, failed\n", devaddr, regaddr, ret);
    return ret;
}

static int lsw_indirect_write(struct mii_bus *bus, int devaddr, int regaddr, u16 value)
{
    int ret;

    ret = lsw_direct_write(bus, SMI_GLOBAL2_DEVADDR, SMI_PHY_DATA_REG, value);
    if (ret) {
        lsw_err("write: devaddr=0x%x regaddr=0x%x ret=%d, failed\n", devaddr, regaddr, ret);
        return ret;
    }
    ret = lsw_indirect_cmd(bus, devaddr, regaddr, SMI_WRITE);
    return ret;
}

static int lsw_check_present(struct mii_bus *bus)
{
    int lsw_version;

    lsw_version = lsw_direct_read(bus, SMI_PORT_DEVADDR(0), SMI_PORT_ID_REG);
    lsw_info("switch id : 0x%x\n", lsw_version);
    /* Check lsw is mv88e6352 */
    if (lsw_version != LSW_PHY_ID) {
        return -ENODEV;
    }

    return 0;
}

/* when using two-pairs cables, shoule enable downshift */
STATIC int lsw_port_downshift(struct mii_bus *bus, int port)
{
    u16 value;
    int ret;

    /* enable downshift */
    ret = lsw_indirect_read(bus, SMI_PHY_DEVADDR(port), SMI_SPEC_CR_REG);
    if (ret < 0)
        return ret;
    value = (u16)ret;
    value |= (1 << 11);
    ret = lsw_indirect_write(bus, SMI_PHY_DEVADDR(port), SMI_SPEC_CR_REG, value);
    if (ret < 0)
        return ret;

    /* Restart Auto-Negotiation Process  & softreset */
    ret = lsw_indirect_read(bus, SMI_PHY_DEVADDR(port), MII_BMCR);
    if (ret < 0)
        return ret;
    value = (u16)ret;
    value |= BMCR_ANRESTART; /* bit 9:restart auto-neg */
    value |= BMCR_RESET;     /* bit 15: softreset */
    ret = lsw_indirect_write(bus, SMI_PHY_DEVADDR(port), MII_BMCR, value);
    mdelay(100);

    return ret;
}

STATIC int lsw_port_powerdown(struct mii_bus *bus, int port)
{
    u16 value;
    int ret;

    /* 88e6352 power down is controlled via 0_0.11 and 16_0.2 */
    ret = lsw_indirect_read(bus, SMI_PHY_DEVADDR(port), SMI_SPEC_CR_REG);
    if (ret < 0)
        return ret;
    /* set 1 to enable power down */
    value = (u16)ret;
    value |= (1 << 2);
    ret = lsw_indirect_write(bus, SMI_PHY_DEVADDR(port), SMI_SPEC_CR_REG, value);
    if (ret < 0)
        return ret;

    ret = lsw_indirect_read(bus, SMI_PHY_DEVADDR(port), MII_BMCR);
    if (ret < 0)
        return ret;
    /* set 1 to enable power down */
    value = (u16)ret;
    value |= (1 << 11);
    ret = lsw_indirect_write(bus, SMI_PHY_DEVADDR(port), MII_BMCR, value);

    return ret;
}

static int lsw_config(struct lsw_dev *lsw)
{
    struct mii_bus *bus;
    int value;
    int ret;

    bus = lsw->bus;

    ret = lsw_check_present(bus);
    if (ret) {
        lsw_info("lsw is not present.\n");
        return 0;
    }
    ret = lsw_port_downshift(bus, LSW_PORT_0);
    CHECK_GOTO((ret == 0), lsw_config_fail);
    ret = lsw_port_downshift(bus, LSW_PORT_1);
    CHECK_GOTO((ret == 0), lsw_config_fail);
    ret = lsw_port_powerdown(bus, LSW_PORT_2);
    CHECK_GOTO((ret == 0), lsw_config_fail);
    ret = lsw_port_powerdown(bus, LSW_PORT_3);
    CHECK_GOTO((ret == 0), lsw_config_fail);
    ret = lsw_port_powerdown(bus, LSW_PORT_4);
    CHECK_GOTO((ret == 0), lsw_config_fail);

    ret = lsw_direct_write(bus, SMI_PORT_DEVADDR(LSW_PORT_5), 0x1, 0xc0fe);
    CHECK_GOTO((ret == 0), lsw_config_fail);
    ret = lsw_direct_write(bus, SMI_PORT_DEVADDR(LSW_PORT_5), 0x4, 0xf);
    CHECK_GOTO((ret == 0), lsw_config_fail);
    ret = lsw_direct_write(bus, SMI_PORT_DEVADDR(LSW_PORT_6), 0x1, 0xc0fe);
    CHECK_GOTO((ret == 0), lsw_config_fail);
    ret = lsw_direct_write(bus, SMI_PORT_DEVADDR(LSW_PORT_6), 0x4, 0xf);
    CHECK_GOTO((ret == 0), lsw_config_fail);

    value = lsw_direct_read(bus, SMI_PORT_DEVADDR(LSW_PORT_6), 0x1);
    lsw_info("Port6 CHECK [0x16:0x01]=0x%x\n", value);

    lsw_info("lsw_config ok.\n");
    return 0;

lsw_config_fail:
    lsw_err("lsw_config failed, ret = %d.\n", ret);
    return ret;
}

static int lsw_ioctl_cmd(struct mii_bus *bus, unsigned int cmd, struct lsw_arg *lsw_status)
{
    int data;
    int ret = 0;

    switch (cmd) {
        case LSW_IO_READ:
            if ((lsw_status->devaddr <= 4) || (lsw_status->devaddr == 0xF))
                data = lsw_indirect_read(bus, (int)lsw_status->devaddr, (int)lsw_status->regaddr);
            else
                data = lsw_direct_read(bus, (int)lsw_status->devaddr, (int)lsw_status->regaddr);

            if (data < 0) {
                return data;
            }
            lsw_status->value_out = (u16)data;
            break;
        case LSW_IO_WRITE:
            if ((lsw_status->devaddr <= 4) || (lsw_status->devaddr == 0xF))
                ret = lsw_indirect_write(bus, (int)lsw_status->devaddr, (int)lsw_status->regaddr, lsw_status->value_in);
            else
                ret = lsw_direct_write(bus, (int)lsw_status->devaddr, (int)lsw_status->regaddr, lsw_status->value_in);
            break;
        default:
            ret = -EFAULT;
    }

    return ret;
}

STATIC long lsw_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
{
    struct lsw_arg argp;
    struct mii_bus *bus = NULL;
    int ret;

    if ((NULL == (void __user *)(uintptr_t)arg) || (NULL == lsw)) {
        lsw_err("IOCTL arg is null or lsw is NULL, lswp=0x%p\n", lsw);
        return -1;
    }

    memset_s(&argp, sizeof(struct lsw_arg), 0, sizeof(struct lsw_arg));
    bus = lsw->bus;

    if (copy_from_user((void *)&argp, (void *)(uintptr_t)arg, sizeof(struct lsw_arg))) {
        lsw_err("copy_from_user fail!\n");
        return -1;
    }

    ret = lsw_ioctl_cmd(bus, cmd, &argp);
    if (ret) {
        lsw_err("ioctl command failed, ret = %d\n", ret);
        return -1;
    }

    if (copy_to_user((void *)(uintptr_t)arg, (void *)&argp, sizeof(struct lsw_arg))) {
        lsw_err("copy_to_user fail!\n");
        return -1;
    }

    return ret;
}

static const struct file_operations lsw_ops = {
    .owner = THIS_MODULE,
    .unlocked_ioctl = lsw_ioctl,
};

STATIC int lsw_cdev_create(struct lsw_dev *lsw)
{
    struct device *drv_dev = NULL;
    int ret;

    if (lsw == NULL)
        return -ENOMEM;

    ret = alloc_chrdev_region(&lsw->devno, 0, LSW_CDEV_MINORS, LSW_CDEV_NAME);
    if (ret) {
        lsw_err("alloc_chrdev_region failed, ret = %d\n", ret);
        goto _out;
    }

    lsw->lsw_cdev_cls = class_create(THIS_MODULE, LSW_CDEV_NAME);
    if (IS_ERR(lsw->lsw_cdev_cls)) {
        lsw_err("class_create faild, ret = %pK\n", lsw->lsw_cdev_cls);
        goto _unregister_region;
    }

    drv_dev = device_create(lsw->lsw_cdev_cls, NULL, lsw->devno, NULL, LSW_CDEV_NAME);
    if (IS_ERR(drv_dev)) {
        lsw_err("failed to create device, ret = %ld\n", PTR_ERR(drv_dev));
        goto _class_del;
    }

    cdev_init(&lsw->lsw_cdev, &lsw_ops);
    lsw->lsw_cdev.owner = THIS_MODULE;
    ret = cdev_add(&lsw->lsw_cdev, lsw->devno, LSW_CDEV_MINORS);
    if (ret) {
        lsw_err("cdev_add failed, ret = %d\n", ret);
        goto _device_del;
    }

    lsw_info("lsw cdev create finish, name is %s\n", LSW_CDEV_NAME);
    return 0;

_device_del:
    device_destroy(lsw->lsw_cdev_cls, lsw->devno);
_class_del:
    class_destroy(lsw->lsw_cdev_cls);
_unregister_region:
    unregister_chrdev_region(lsw->devno, LSW_CDEV_MINORS);
_out:
    return -1;
}

STATIC void lsw_cdev_del(struct lsw_dev *lsw)
{
    if (lsw == NULL)
        return;
    cdev_del(&lsw->lsw_cdev);
    device_destroy(lsw->lsw_cdev_cls, lsw->devno);
    class_destroy(lsw->lsw_cdev_cls);
    unregister_chrdev_region(lsw->devno, LSW_CDEV_MINORS);
    lsw->lsw_cdev_cls = NULL;

    lsw_info("lsw cdev del finish\n");
}

STATIC int __init lsw_init(void)
{
    int ret = -EFAULT;

    lsw = (struct lsw_dev *)kzalloc(sizeof(struct lsw_dev), GFP_KERNEL);
    if (NULL == lsw)
        return -ENOMEM;

    lsw->bus = hns_get_mdiobus();
    if (NULL == lsw->bus)
        goto __lsw_del;

    ret = lsw_config(lsw);
    if (ret)
        goto __lsw_del;
    ret = lsw_cdev_create(lsw);
    if (ret)
        goto __lsw_del;

    lsw_info("lsw_init success\n");
    return 0;

__lsw_del:
    kfree(lsw);
    lsw = NULL;
    return ret;
}

STATIC void __exit lsw_exit(void)
{
    lsw_cdev_del(lsw);
    if (NULL != lsw) {
        kfree(lsw);
        lsw = NULL;
    }
}

module_init(lsw_init);
module_exit(lsw_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Huawei Tech. Co., Ltd.");
MODULE_DESCRIPTION("Hisilicon LAN SWITCH driver");
